package com.gusto.engine.evaluation.service.impl;

import java.util.List;

import org.apache.log4j.Logger;

import com.gusto.engine.evaluation.RealEvaluation;
import com.gusto.engine.evaluation.data.DataDAO;
import com.gusto.engine.evaluation.indicators.Coverage;
import com.gusto.engine.evaluation.indicators.MAE;
import com.gusto.engine.evaluation.indicators.RMSE;
import com.gusto.engine.evaluation.service.EvaluationService;

/**
 * <p>Implements the {@link EvaluationService}. 
 * Also implements the 3 measures: {@link MAE} {@link RMSE} {@link Coverage}.</p>
 * 
 * @author amokrane.belloui@gmail.com
 *
 */
public class EvaluationServiceImpl implements EvaluationService, RMSE, MAE, Coverage {
	
	private Logger log = Logger.getLogger(getClass()); 
	
	private DataDAO dataDAO;
	public void setDataDAO(DataDAO dataDAO) {
		this.dataDAO = dataDAO;
	}
	
	private Long MIN_USER = Long.MIN_VALUE;
	public void setMIN_USER(Long min_user) {
		MIN_USER = min_user;
	}
	
	private Long MAX_USER = Long.MAX_VALUE;
	public void setMAX_USER(Long max_user) {
		MAX_USER = max_user;
	}
	
	private Double ALLOWED_ERROR = Double.MAX_VALUE;
	public void setALLOWED_ERROR(Double allowed_error) {
		ALLOWED_ERROR = allowed_error;
	}
	
	private long start;
	private long end;
	
	private long total = 0;
	private long calculated = 0;
	
	private double e = 0;
	private double sumDiff = 0;
	private double ratingcount = 0;
	
	private double rmse;
	private double mae;
	
	
	public EvaluationServiceImpl() {
	}
	
	public void start() {
		start = System.currentTimeMillis();
		total = 0;
		calculated = 0;
		e = 0;
		sumDiff = 0;
		ratingcount = 0;
		log.info("Started Evaluation");
	}
	
	public List<RealEvaluation> getData() {
		log.info("Getting the Test Data (" + MIN_USER + "|" + MAX_USER + ")");
		return dataDAO.getData(MIN_USER, MAX_USER);
	}
	
	public void addEvaluation(RealEvaluation real, Double prediction) {
		this.total ++;
		if ((real != null) && (prediction != null)) {
			if (!prediction.equals(Double.NaN)) {
				e += Math.pow(real.getEvaluation() - prediction, 2);
				sumDiff += Math.abs(real.getEvaluation() - prediction);
				this.ratingcount++;
				this.rmse = Math.sqrt(e / ratingcount);
				this.mae = sumDiff / ratingcount;
				this.calculated ++;
				
				logActualValues(rmse, mae, ratingcount, System.currentTimeMillis() - start);					
				logFlagrantErrors(real, prediction, this.ALLOWED_ERROR);
			}
		}
		this.rmse = Math.sqrt(e / ratingcount); 
		this.mae = sumDiff / ratingcount;
	}
	
	public void finish() {
		end = System.currentTimeMillis();
		log.info("Finished Evaluation");
		
		log.info(ratingcount + " ratings evaluated in " + ((end - start)/1000) + " seconds.");
		log.info("RMSE: " + getRMSE());
		log.info("MAE : " + getMAE());
		log.info("Coverage: " + getCoverage() + ", " + this.calculated + "/" + this.total);
	}
	
	private long checkpoint1 = System.currentTimeMillis();
	private void logActualValues(double rmse, double mae, double ratingcount, long timeMillis) {
		boolean isLog = true;
		if (isLog) {
			if ((System.currentTimeMillis() - checkpoint1) > 3000) {
				String results = "[RMSE " + round(getRMSE()) + "]" + 
				                 "[MAE " +  round(getMAE()) + "]" + 
				                 "[Coverage " +  round(getCoverage()) + ", " + calculated + "/" + total + "] " +
				                 "in " + (timeMillis/1000) + " sec";
				log.info(results);
				checkpoint1 = System.currentTimeMillis();
			}
		}
	}
	
	private void logFlagrantErrors(RealEvaluation real, Double prediction, double max_error) {
		// Logging Flagrant errors
		if (Math.abs(real.getEvaluation() - prediction) >= max_error) {
			String flagrant = "--> " + real.getUser() + "-" + real.getItem() + " " +  
							  prediction + " / " + real.getEvaluation();
			log.warn(flagrant);
		}
	}
	
	private double round(double val) {
		return Math.round(val * 10000) / 10000.0;
	}
	
	public Double getRMSE() {
		return rmse;
	}

	public Double getMAE() {
		return mae;
	}
	
	public Double getCoverage() {
		return ((this.calculated*1.0) / (this.total*1.0)) * 100;
	}
	
	public Long getTotal() {
		return this.total;
	}
	
	public Long getCalculated() {
		return this.calculated;
	}
}
